package edu.buaa.act.helper;

import com.graphhopper.util.DistanceCalc;
import com.graphhopper.util.DistancePlaneProjection;
import edu.buaa.act.model.GPSPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;



/**
 * Created by T7810 on 2017/4/21.
 *
 * Including:
 * 1. convert long car id to int car id (range from 1 to 94)
 * 2. remove direction and angle field
 * 3. find common cover time range
 * 4.
 */
public class RawDataCleaning
{
    private static Logger log = Utils.getDebugLogger();

    /**
     * remove all gpsPoint which out of given bounds.
     * @return a new list contains valid gps point.
     */
    public static List<GPSPoint> filterByGeoBound(List<GPSPoint> rawData, double minLat, double maxLat, double minLon, double maxLon)
    {
        double minLatitude =Double.MAX_VALUE;
        double minLongitude =Double.MAX_VALUE;
        double maxLatitude =Double.MIN_VALUE;
        double maxLongitude =Double.MIN_VALUE;

        List<GPSPoint> result = new ArrayList<>();
        for(GPSPoint p : rawData)
        {
            double lat = p.getLatitude();
            double lon = p.getLongitude();

            if(lat>maxLatitude) maxLatitude=lat;
            if(lat<minLatitude) minLatitude=lat;
            if(lon>maxLongitude) maxLongitude=lon;
            if(lon<minLongitude) minLongitude=lon;
            if(minLat<lat  && lat<maxLat && minLon<lon && lon<maxLon)
            {
                result.add(p);
            }
        }
        log.trace("original bound: ({}, {}, {}, {}) is cut to ({}, {}, {}, {})",
                minLatitude, maxLatitude, minLongitude, maxLongitude,
                minLat, maxLat, minLon, maxLon);
        log.trace("remove {} gps points. {}/{} point remains",
                rawData.size()-result.size(), result.size(), rawData.size());
        return result;
    }

    /**
     * remove a car's all trajectories if its time not cover given time intervals
     * @param start
     * @param end
     * @throws ParseException
     */
    public static List<List<GPSPoint>> filterByTime(List<List<GPSPoint>> dataSeparateByCar, String start, String end) throws ParseException
    {
        long startTimestamp = Utils.timeStr2stamp(start);
        long endTimestamp = Utils.timeStr2stamp(end);

        List<List<GPSPoint>> remained = new ArrayList<>();
        for(List<GPSPoint> traj : dataSeparateByCar) {
            long trajStart = traj.get(0).getTimestamp();
            long trajEnd = traj.get(traj.size() - 1).getTimestamp();
            if ( trajStart <= startTimestamp && endTimestamp <= trajEnd ) {
                remained.add(traj);
            }
        }
        log.trace("{} traj remains. {} trajectories which not cover {} to {} is removed.",
                remained.size(), dataSeparateByCar.size()-remained.size(),  start, end);
        return remained;
    }

    public static void assertTimeInc(List<List<GPSPoint>> dataSeparateByCar)
    {
        for(List<GPSPoint> traj : dataSeparateByCar){
            for(int i=1; i<traj.size(); i++){
                if(traj.get(i).getTimestamp() <= traj.get(i-1).getTimestamp()){
                    throw new RuntimeException(String.format(
                            "time in one traj not inc! (%d:%d) pre: %d %f %f, cur: %d %f %f",
                            i,
                            traj.get(i).getCarNo(),
                            traj.get(i-1).getTimestamp(),
                            traj.get(i-1).getLatitude(),
                            traj.get(i-1).getLongitude(),
                            traj.get(i).getTimestamp(),
                            traj.get(i).getLatitude(),
                            traj.get(i).getLongitude()));
                }
            }
        }
    }

    public static List<List<GPSPoint>> separateTrajByCarIDReverse(List<GPSPoint> rawData)
    {
        List<List<GPSPoint>> result = new ArrayList<>();
        List<GPSPoint> oneCarTraj = null;
        long lastCar=-1;
        for(int i=rawData.size()-1; i>=0; i--){ // reverse because timestamp is dec.
            GPSPoint p = rawData.get(i);
            if(i==rawData.size()-1){
                lastCar = p.getCarNo();
                oneCarTraj = new ArrayList<>();
                oneCarTraj.add(p);
                result.add(oneCarTraj);
            }else{
                long car = p.getCarNo();
                if(car==lastCar){
                    oneCarTraj.add(p);
                }else{
                    oneCarTraj = new ArrayList<>();
                    oneCarTraj.add(p);
                    result.add(oneCarTraj);
                }
                lastCar = car;
            }
        }
        log.trace("data separated to {} car's long trajectories", result.size());
        return result;
    }

    public static List<List<GPSPoint>> separateTrajByCarID(List<GPSPoint> rawData)
    {
        List<List<GPSPoint>> result = new ArrayList<>();
        List<GPSPoint> oneCarTraj = null;
        long lastCar=-1;
        for(int i=0; i<rawData.size(); i++){ // reverse because timestamp is dec.
            GPSPoint p = rawData.get(i);
            if(i==0){
                lastCar = p.getCarNo();
                oneCarTraj = new ArrayList<>();
                oneCarTraj.add(p);
                result.add(oneCarTraj);
            }else{
                long car = p.getCarNo();
                if(car==lastCar){
                    oneCarTraj.add(p);
                }else{
                    oneCarTraj = new ArrayList<>();
                    oneCarTraj.add(p);
                    result.add(oneCarTraj);
                }
                lastCar = car;
            }
        }
        log.trace("data separated to {} car's long trajectories", result.size());
        return result;
    }

    public static long[] findCommonTime(List<List<GPSPoint>> dataSeparateByCar)
    {
        long latestStartTime = Long.MIN_VALUE;
        long earliestEndTime = Long.MAX_VALUE;
        boolean first = true;
        for(List<GPSPoint> traj : dataSeparateByCar){
            long start = traj.get(0).getTimestamp();
            long end = traj.get(traj.size() - 1).getTimestamp();
            if(first) {
                latestStartTime=start;
                earliestEndTime=end;
                first=false;
            }else {
                if (start > latestStartTime) {
                    latestStartTime = start;
                }
                if (end < earliestEndTime) {
                    earliestEndTime = end;
                }
            }
        }


        log.trace("common time interval found: {} to {}",
                Utils.timestamp2Str(latestStartTime),
                Utils.timestamp2Str(earliestEndTime));
        if(latestStartTime>=earliestEndTime){
            throw new RuntimeException("Trajectory Intervals not overlap!");
        }
        return new long[]{latestStartTime, earliestEndTime};
    }

    public static void setCarID(List<GPSPoint> rawData)
    {
        Map<Long, Integer> idMap = getUniqueIDs(rawData);
        setCarID(idMap, rawData);
    }

    public static void setCarID(Map<Long, Integer> idMap, List<GPSPoint> rawData)
    {
        for(GPSPoint point: rawData){
            Integer id = idMap.get(point.getCarNo());
            if(id==null || id==0){
                throw new RuntimeException("id not exist! should not happen!");
            }
            point.setCarID(id);
        }
    }

    public static Map<Long, Integer> getUniqueIDs(List<GPSPoint> gpsPoints)
    {
        Set<Long> allCarIdSet = new TreeSet<Long>();
        for(GPSPoint point : gpsPoints) {
            allCarIdSet.add(point.getCarNo());
        }

        Map<Long, Integer> map = new HashMap<>();
        int id=1;
        for(Long no : allCarIdSet){
            map.put(no, id);
            id++;
        }
        return map;
    }

    public static void printInfo(List<List<GPSPoint>> dataSeparateByCar)
    {
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm");
        df.setTimeZone(TimeZone.getTimeZone("GMT+8:00")); //UTC
        log.debug("carID  gpsPointCount  startTimestamp  endTimestamp  timeInterval(seconds)");
        for(List<GPSPoint> traj : dataSeparateByCar)
        {
            for(GPSPoint p : traj){
                if(p.getCarID()==0){
                    throw new RuntimeException("car id is 0");
                }
            }
            log.debug("{} {} {} {} {}",
                    traj.get(0).getCarID(),
                    traj.size(),
                    df.format(traj.get(0).getTimestamp()),
                    df.format(traj.get(traj.size()-1).getTimestamp()),
                    (traj.get(traj.size()-1).getTimestamp()- traj.get(0).getTimestamp())/1000
            );
        }
    }

    public static void assertSpeedLimit(List<List<GPSPoint>> data1) {
        DistanceCalc distanceCalc = new DistancePlaneProjection();
        boolean hasErr=false;
        for(List<GPSPoint> traj : data1){
            for(int i=1; i<traj.size(); i++){
                double lat0 = traj.get(i - 1).getLatitude();
                double lon0 = traj.get(i - 1).getLongitude();
                double lat1 = traj.get(i).getLatitude();
                double lon1 = traj.get(i).getLongitude();
                double dis = distanceCalc.calcDist(lat0, lon0, lat1, lon1);
                long time = traj.get(i).getTimestamp() - traj.get(i-1).getTimestamp();
                double speed = dis/time;
                if(speed>70) {
                    log.debug(
                            "speed too high (may be mixed trajs). " +
                                    "speed({}), distance({}), time({}), carID({}), carNO({}), timestamp({})",
                            speed,dis,time,
                            traj.get(i-1).getCarID(),
                            traj.get(i-1).getCarNo(),
                            traj.get(i-1).getTimestamp()
                    );
                    hasErr=true;
                }
            }
        }
        if(hasErr) {
            throw new RuntimeException("speed too high");
        }else{
            log.info("speed checked, all less than 70m/s");
        }
    }
}
